"use strict"; /* ========================================================= HEXORA SEARCH — MAIN.JS ========================================================= */ const CONFIG = { searchEndpoint: "/api/search", newsEndpoint: "/api/news", map: { defaultCenter: [91.7362, 26.1445], defaultZoom: 5, osmTiles: "https://tile.openstreetmap.org/{z}/{x}/{y}.png", satelliteTiles: "https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}", geocoder: "https://nominatim.openstreetmap.org/search" } }; let map = null; let mapMarker = null; /* ========================================================= HELPERS ========================================================= */ function $(selector) { return document.querySelector(selector); } function $$(selector) { return Array.from(document.querySelectorAll(selector)); } function escapeHTML(value) { return String(value ?? "") .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """) .replace(/'/g, "'"); } function safeURL(url) { try { const value = String(url || "").trim(); if (!value) { return "#"; } if ( value.startsWith("http://") || value.startsWith("https://") ) { return value; } return new URL(value, window.location.origin).href; } catch { return "#"; } } function formatDate(value) { if (!value) return ""; const date = new Date(value); if (Number.isNaN(date.getTime())) { return String(value); } return date.toLocaleDateString("en-IN", { day: "numeric", month: "short", year: "numeric" }); } /* ========================================================= MAPLIBRE ========================================================= */ function loadMapLibre() { return new Promise((resolve, reject) => { if (window.maplibregl) { resolve(window.maplibregl); return; } const oldScript = document.querySelector( 'script[data-hexora-maplibre="true"]' ); if (oldScript) { oldScript.addEventListener("load", () => { resolve(window.maplibregl); }); oldScript.addEventListener("error", reject); return; } const script = document.createElement("script"); script.src = "https://unpkg.com/maplibre-gl@4.7.1/dist/maplibre-gl.js"; script.async = true; script.dataset.hexoraMaplibre = "true"; script.onload = () => { if (window.maplibregl) { resolve(window.maplibregl); } else { reject(new Error("MapLibre failed to load.")); } }; script.onerror = () => { reject(new Error("Could not load MapLibre.")); }; document.head.appendChild(script); }); } /* ========================================================= MAP STYLES ========================================================= */ function streetStyle() { return { version: 8, sources: { hexoraOSM: { type: "raster", tiles: [CONFIG.map.osmTiles], tileSize: 256, attribution: "© OpenStreetMap contributors" } }, layers: [ { id: "hexora-osm", type: "raster", source: "hexoraOSM" } ] }; } function satelliteStyle() { return { version: 8, sources: { hexoraSatellite: { type: "raster", tiles: [CONFIG.map.satelliteTiles], tileSize: 256, attribution: "© Esri" } }, layers: [ { id: "hexora-satellite", type: "raster", source: "hexoraSatellite" } ] }; } /* ========================================================= INITIALIZE MAP ========================================================= */ async function initializeMap() { const container = $("#hexoraMap"); if (!container) { return null; } if (map) { return map; } try { const maplibregl = await loadMapLibre(); map = new maplibregl.Map({ container: "hexoraMap", style: streetStyle(), center: CONFIG.map.defaultCenter, zoom: CONFIG.map.defaultZoom, pitch: 0, bearing: 0, attributionControl: true }); map.addControl( new maplibregl.NavigationControl({ visualizePitch: true }), "top-right" ); map.on("error", (event) => { console.error("HEXORA Map error:", event); }); return map; } catch (error) { console.error("HEXORA Map loading error:", error); const info = $("#mapInfo"); if (info) { info.textContent = "HEXORA Map could not be loaded."; info.style.display = "block"; } return null; } } /* ========================================================= OPEN MAP ========================================================= */ async function openMap(options = {}) { const mapView = $("#mapView"); const searchView = $("#searchView"); if (!mapView) { return; } if (searchView) { searchView.classList.remove("active"); } const home = $("#homeView"); if (home) { home.style.display = "none"; } mapView.classList.add("active"); const instance = await initializeMap(); if (!instance) { return; } setTimeout(() => { instance.resize(); if (options.center) { instance.flyTo({ center: options.center, zoom: options.zoom || 13, speed: 1.2, essential: true }); } }, 100); } /* ========================================================= CLOSE MAP ========================================================= */ function closeMap() { const mapView = $("#mapView"); if (mapView) { mapView.classList.remove("active"); } } /* ========================================================= MAP STYLE CHANGE ========================================================= */ function changeMapStyle(type) { if (!map) { return; } const center = map.getCenter().toArray(); const zoom = map.getZoom(); const pitch = map.getPitch(); const bearing = map.getBearing(); if (type === "satellite") { map.setStyle(satelliteStyle()); } else { map.setStyle(streetStyle()); } map.once("style.load", () => { map.jumpTo({ center, zoom, pitch, bearing }); }); } /* ========================================================= MAP SEARCH ========================================================= */ async function searchMapPlace(query) { const value = String(query || "").trim(); if (!value) { return; } const info = $("#mapInfo"); if (info) { info.textContent = "Searching..."; info.style.display = "block"; } try { const url = new URL(CONFIG.map.geocoder); url.searchParams.set("q", value); url.searchParams.set("format", "json"); url.searchParams.set("limit", "5"); url.searchParams.set("addressdetails", "1"); const response = await fetch(url.toString(), { headers: { Accept: "application/json" } }); if (!response.ok) { throw new Error("Map search failed."); } const places = await response.json(); if (!Array.isArray(places) || places.length === 0) { if (info) { info.textContent = "Place not found."; } return; } const place = places[0]; const lat = Number(place.lat); const lon = Number(place.lon); if (!Number.isFinite(lat) || !Number.isFinite(lon)) { throw new Error("Invalid coordinates."); } await openMap({ center: [lon, lat], zoom: 13 }); if (!map) { return; } map.flyTo({ center: [lon, lat], zoom: 13, speed: 1.2, essential: true }); addMapMarker([lon, lat]); if (info) { info.textContent = place.display_name || value; } } catch (error) { console.error("HEXORA map search error:", error); if (info) { info.textContent = "Map search failed. Please try again."; } } } /* ========================================================= MAP MARKER ========================================================= */ function addMapMarker(coordinates) { if (!map || !window.maplibregl) { return; } if (mapMarker) { mapMarker.remove(); } mapMarker = new window.maplibregl.Marker() .setLngLat(coordinates) .addTo(map); } /* ========================================================= LOCATION ========================================================= */ function locateUser() { if (!navigator.geolocation) { alert( "Your browser does not support location." ); return; } const info = $("#mapInfo"); if (info) { info.textContent = "Getting your location..."; info.style.display = "block"; } navigator.geolocation.getCurrentPosition( async (position) => { const latitude = position.coords.latitude; const longitude = position.coords.longitude; await openMap({ center: [longitude, latitude], zoom: 15 }); if (!map) { return; } map.flyTo({ center: [longitude, latitude], zoom: 15, speed: 1.2, essential: true }); addMapMarker([ longitude, latitude ]); if (info) { info.textContent = "Your current location"; } }, (error) => { console.error( "HEXORA location error:", error ); if (info) { info.textContent = "Location permission denied or unavailable."; } }, { enableHighAccuracy: true, timeout: 15000, maximumAge: 0 } ); } /* ========================================================= RESET MAP ========================================================= */ function resetMap() { if (!map) { return; } if (mapMarker) { mapMarker.remove(); mapMarker = null; } map.flyTo({ center: CONFIG.map.defaultCenter, zoom: CONFIG.map.defaultZoom, pitch: 0, bearing: 0, speed: 1.2, essential: true }); const info = $("#mapInfo"); if (info) { info.textContent = ""; info.style.display = "none"; } } /* ========================================================= 3D VIEW ========================================================= */ function enable3D() { if (!map) { return; } map.easeTo({ pitch: 60, bearing: -20, duration: 1000 }); } /* ========================================================= FULLSCREEN ========================================================= */ function fullscreenMap() { const mapView = $("#mapView"); if (!mapView) { return; } if (!document.fullscreenElement) { if (mapView.requestFullscreen) { mapView.requestFullscreen(); } } else { document.exitFullscreen(); } setTimeout(() => { if (map) { map.resize(); } }, 500); } /* ========================================================= SEARCH ========================================================= */ async function performSearch(query) { const value = String(query || "").trim(); if (!value) { return; } closeMap(); const home = $("#homeView"); const searchView = $("#searchView"); if (home) { home.style.display = "none"; } if (searchView) { searchView.classList.add("active"); } const resultMeta = $("#resultMeta"); const results = $("#results"); if (resultMeta) { resultMeta.textContent = `Searching for "${value}"...`; } if (results) { results.innerHTML = `
Searching HEXORA...
`; } try { const url = new URL( CONFIG.searchEndpoint, window.location.origin ); url.searchParams.set("q", value); const response = await fetch( url.toString(), { headers: { Accept: "application/json" } } ); if (!response.ok) { throw new Error( `Search HTTP ${response.status}` ); } const data = await response.json(); const items = Array.isArray(data) ? data : Array.isArray(data.results) ? data.results : Array.isArray(data.data) ? data.data : []; renderSearchResults(items, value); } catch (error) { console.error( "HEXORA Search error:", error ); if (resultMeta) { resultMeta.textContent = `Search: "${value}"`; } if (results) { results.innerHTML = `

HEXORA Search Error

Search service is temporarily unavailable. Please try again.

`; } } } /* ========================================================= SEARCH RESULTS ========================================================= */ function renderSearchResults(items, query) { const resultMeta = $("#resultMeta"); const results = $("#results"); if (resultMeta) { resultMeta.textContent = `${items.length} result${items.length === 1 ? "" : "s"} for "${query}"`; } if (!results) { return; } if (!items.length) { results.innerHTML = `

No results found

Try another search query.

`; return; } results.innerHTML = items .map((item) => { const title = item.title || item.name || "Untitled"; const description = item.description || item.snippet || item.content || ""; const url = item.url || item.link || "#"; const source = item.source_name || item.source || item.source_domain || ""; const date = item.published_at || item.date || item.publishedAt || ""; return `
${escapeHTML(source)}

${escapeHTML(title)}

${escapeHTML(url)}

${escapeHTML(description)}

${ date ? `
${escapeHTML( formatDate(date) )}
` : "" }
`; }) .join(""); } /* ========================================================= NEWS ========================================================= */ async function loadNews() { const newsList = $("#newsList"); if (!newsList) { return; } try { const response = await fetch( CONFIG.newsEndpoint, { headers: { Accept: "application/json" } } ); if (!response.ok) { throw new Error( `News HTTP ${response.status}` ); } const data = await response.json(); const items = Array.isArray(data) ? data : Array.isArray(data.results) ? data.results : Array.isArray(data.data) ? data.data : []; renderNews(items); } catch (error) { console.error( "HEXORA News error:", error ); newsList.innerHTML = `
News is temporarily unavailable.
`; } } /* ========================================================= RENDER NEWS ========================================================= */ function renderNews(items) { const newsList = $("#newsList"); if (!newsList) { return; } if (!items.length) { newsList.innerHTML = `
No news available right now.
`; return; } newsList.innerHTML = items .slice(0, 20) .map((item) => { const title = item.title || "Untitled news"; const description = item.description || item.snippet || ""; const url = item.url || item.link || "#"; const image = item.image_url || item.image || ""; const source = item.source_name || item.source || item.source_domain || ""; const date = item.published_at || item.date || ""; return `
${ image ? ` ${escapeHTML(title)} ` : "" }
${ source ? `
${escapeHTML(source)}
` : "" }

${escapeHTML(title)}

${ description ? `

${escapeHTML( description )}

` : "" } ${ date ? ` ${escapeHTML( formatDate(date) )} ` : "" }
`; }) .join(""); } /* ========================================================= HOME ========================================================= */ function showHome() { const home = $("#homeView"); const searchView = $("#searchView"); const mapView = $("#mapView"); if (home) { home.style.display = ""; } if (searchView) { searchView.classList.remove("active"); } if (mapView) { mapView.classList.remove("active"); } window.scrollTo({ top: 0, behavior: "smooth" }); } /* ========================================================= SEARCH FORM ========================================================= */ function setupSearch() { const form = $("#searchForm"); const input = $("#searchInput"); if (!form || !input) { return; } form.addEventListener( "submit", (event) => { event.preventDefault(); const query = input.value.trim(); if (!query) { input.focus(); return; } performSearch(query); } ); } /* ========================================================= MAP SEARCH FORM ========================================================= */ function setupMapSearch() { const form = $(".map-search-panel"); const input = $("#mapSearchInput"); const button = $("#mapSearchBtn"); if (form && input) { form.addEventListener( "submit", (event) => { event.preventDefault(); searchMapPlace(input.value); } ); } if (button && input) { button.addEventListener( "click", () => { searchMapPlace(input.value); } ); } } /* ========================================================= NAVIGATION ========================================================= */ function setupNavigation() { $$("[data-mode]").forEach((button) => { button.addEventListener( "click", () => { const mode = button.dataset.mode; if (mode === "maps") { openMap(); return; } const input = $("#searchInput"); if (!input) { return; } if (mode === "news") { input.value = "latest news"; } else if (mode === "images") { input.value = "images"; } else if (mode === "videos") { input.value = "videos"; } else if (mode === "shopping") { input.value = "shopping"; } input.focus(); } ); }); $$("[data-trending]").forEach( (button) => { button.addEventListener( "click", () => { const query = button.dataset.trending || button.textContent.trim(); const input = $("#searchInput"); if (input) { input.value = query; } performSearch(query); } ); } ); $$("[data-quick]").forEach( (button) => { button.addEventListener( "click", () => { const action = button.dataset.quick; if (action === "location") { openMap().then(locateUser); return; } if (action === "place") { openMap(); setTimeout(() => { const input = $("#mapSearchInput"); if (input) { input.focus(); } }, 300); return; } if (action === "directions") { openMap(); setTimeout(() => { const input = $("#mapSearchInput"); if (input) { input.focus(); } }, 300); return; } if (action === "satellite") { openMap().then(() => { changeMapStyle( "satellite" ); }); } } ); } ); } /* ========================================================= MAP BUTTONS ========================================================= */ function setupMapButtons() { const locateBtn = $("#locateBtn"); const resetBtn = $("#resetMapBtn"); const fullscreenBtn = $("#fullscreenMapBtn"); const openMapBtn = $("#openMapBtn"); const previewBtn = $("#mapPreviewBtn"); if (locateBtn) { locateBtn.addEventListener( "click", locateUser ); } if (resetBtn) { resetBtn.addEventListener( "click", resetMap ); } if (fullscreenBtn) { fullscreenBtn.addEventListener( "click", fullscreenMap ); } if (openMapBtn) { openMapBtn.addEventListener( "click", () => openMap() ); } if (previewBtn) { previewBtn.addEventListener( "click", () => openMap() ); } } /* ========================================================= MAP PREVIEW ========================================================= */ async function initializePreviewMap() { const container = $("#mapPreview"); if (!container) { return; } try { const maplibregl = await loadMapLibre(); new maplibregl.Map({ container: "mapPreview", style: satelliteStyle(), center: CONFIG.map.defaultCenter, zoom: 4, interactive: false, attributionControl: false }); } catch (error) { console.error( "HEXORA preview map error:", error ); } } /* ========================================================= BRAND ========================================================= */ function setupBrand() { $$( ".logo, [data-home]" ).forEach((element) => { element.addEventListener( "click", (event) => { event.preventDefault(); showHome(); } ); }); } /* ========================================================= KEYBOARD ========================================================= */ function setupKeyboard() { document.addEventListener( "keydown", (event) => { if ( event.key === "/" && document.activeElement?.tagName !== "INPUT" && document.activeElement?.tagName !== "TEXTAREA" ) { event.preventDefault(); const input = $("#searchInput"); if (input) { input.focus(); } } if (event.key === "Escape") { showHome(); } } ); } /* ========================================================= START HEXORA ========================================================= */ document.addEventListener( "DOMContentLoaded", () => { setupSearch(); setupMapSearch(); setupNavigation(); setupMapButtons(); setupBrand(); setupKeyboard(); loadNews(); initializePreviewMap(); } ); /* ========================================================= HEXORA GLOBAL API ========================================================= */ window.HEXORA = { config: CONFIG, search: performSearch, openMap, closeMap, searchMapPlace, locateUser, resetMap, fullscreenMap, enable3D, satellite() { openMap().then(() => { changeMapStyle("satellite"); }); }, street() { openMap().then(() => { changeMapStyle("street"); }); } };